// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
import 'package:flutter_plugin_tools/src/fetch_deps_command.dart';
import 'package:git/git.dart';
import 'package:test/test.dart';

import 'mocks.dart';
import 'util.dart';

void main() {
  group('FetchDepsCommand', () {
    late Directory packagesDir;
    late CommandRunner<void> runner;
    late MockPlatform mockPlatform;
    late RecordingProcessRunner processRunner;

    setUp(() {
      mockPlatform = MockPlatform();
      final GitDir gitDir;
      (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) =
          configureBaseCommandMocks(platform: mockPlatform);

      final command = FetchDepsCommand(
        packagesDir,
        processRunner: processRunner,
        platform: mockPlatform,
        gitDir: gitDir,
      );

      runner = CommandRunner<void>(
        'fetch_deps_test',
        'Test for $FetchDepsCommand',
      );
      runner.addCommand(command);
    });

    group('dart', () {
      test('runs pub get', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall('flutter', const <String>[
              'pub',
              'get',
            ], plugin.directory.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('runs pub get in non-example sub-packages', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.inline),
          },
        );
        final RepositoryPackage subpackage = createFakePackage(
          'subpackage',
          plugin.directory.childDirectory('extras'),
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall('flutter', const <String>[
              'pub',
              'get',
            ], plugin.directory.path),
            ProcessCall('dart', const <String>[
              'pub',
              'get',
            ], subpackage.directory.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('fails if pub get fails', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        processRunner.mockProcessesForExecutable[getFlutterCommand(
          mockPlatform,
        )] = <FakeProcessInfo>[
          FakeProcessInfo(MockProcess(exitCode: 1)),
        ];

        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>['fetch-deps'],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[contains('Failed to "pub get"')]),
        );
      });

      test(
        'skips unsupported packages when any platforms are passed',
        () async {
          final RepositoryPackage packageWithBoth = createFakePackage(
            'supports_both',
            packagesDir,
            extraFiles: <String>[
              'example/linux/placeholder',
              'example/windows/placeholder',
            ],
          );
          final RepositoryPackage packageWithOne = createFakePackage(
            'supports_one',
            packagesDir,
            extraFiles: <String>['example/linux/placeholder'],
          );
          createFakePackage('supports_neither', packagesDir);

          await runCapturingPrint(runner, <String>[
            'fetch-deps',
            '--linux',
            '--windows',
            '--supporting-target-platforms-only',
          ]);

          expect(
            processRunner.recordedCalls,
            orderedEquals(<ProcessCall>[
              ProcessCall('dart', const <String>[
                'pub',
                'get',
              ], packageWithBoth.path),
              ProcessCall('dart', const <String>[
                'pub',
                'get',
              ], packageWithOne.path),
            ]),
          );
        },
      );
    });

    group('android', () {
      test('runs pub get before gradlew dependencies', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          extraFiles: <String>['example/android/gradlew'],
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Directory androidDir = plugin
            .getExamples()
            .first
            .platformDirectory(FlutterPlatform.android);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--android',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall('flutter', const <String>[
              'pub',
              'get',
            ], plugin.directory.path),
            ProcessCall(androidDir.childFile('gradlew').path, const <String>[
              'plugin1:dependencies',
            ], androidDir.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('runs gradlew dependencies', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          extraFiles: <String>['example/android/gradlew'],
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Directory androidDir = plugin
            .getExamples()
            .first
            .platformDirectory(FlutterPlatform.android);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--android',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall(androidDir.childFile('gradlew').path, const <String>[
              'plugin1:dependencies',
            ], androidDir.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('runs on all examples', () async {
        final examples = <String>['example1', 'example2'];
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          examples: examples,
          extraFiles: <String>[
            'example/example1/android/gradlew',
            'example/example2/android/gradlew',
          ],
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Iterable<Directory> exampleAndroidDirs = plugin.getExamples().map(
          (RepositoryPackage example) =>
              example.platformDirectory(FlutterPlatform.android),
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--android',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            for (final Directory directory in exampleAndroidDirs)
              ProcessCall(directory.childFile('gradlew').path, const <String>[
                'plugin1:dependencies',
              ], directory.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('runs --config-only build if gradlew is missing', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Directory androidDir = plugin
            .getExamples()
            .first
            .platformDirectory(FlutterPlatform.android);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--android',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall(
              getFlutterCommand(mockPlatform),
              const <String>['build', 'apk', '--config-only'],
              plugin.getExamples().first.directory.path,
            ),
            ProcessCall(androidDir.childFile('gradlew').path, const <String>[
              'plugin1:dependencies',
            ], androidDir.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('fails if gradlew generation fails', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        processRunner.mockProcessesForExecutable[getFlutterCommand(
          mockPlatform,
        )] = <FakeProcessInfo>[
          FakeProcessInfo(MockProcess(exitCode: 1)),
        ];

        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>['fetch-deps', '--no-dart', '--android'],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Unable to configure Gradle project'),
          ]),
        );
      });

      test('fails if dependency download finds issues', () async {
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          extraFiles: <String>['example/android/gradlew'],
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final String gradlewPath = plugin
            .getExamples()
            .first
            .platformDirectory(FlutterPlatform.android)
            .childFile('gradlew')
            .path;
        processRunner.mockProcessesForExecutable[gradlewPath] =
            <FakeProcessInfo>[FakeProcessInfo(MockProcess(exitCode: 1))];

        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>['fetch-deps', '--no-dart', '--android'],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('The following packages had errors:'),
          ]),
        );
      });

      test('skips non-Android plugins', () async {
        createFakePlugin('plugin1', packagesDir);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--android',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native Android dependencies.'),
          ]),
        );
      });

      test('skips non-inline plugins', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformAndroid: const PlatformDetails(PlatformSupport.federated),
          },
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--android',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native Android dependencies.'),
          ]),
        );
      });
    });

    group('ios', () {
      test('runs on all examples', () async {
        final examples = <String>['example1', 'example2'];
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          examples: examples,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Iterable<Directory> exampleDirs = plugin.getExamples().map(
          (RepositoryPackage example) => example.directory,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--ios',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            const ProcessCall('flutter', <String>['precache', '--ios'], null),
            const ProcessCall('pod', <String>['repo', 'update'], null),
            for (final Directory directory in exampleDirs)
              ProcessCall('flutter', const <String>[
                'build',
                'ios',
                '--config-only',
              ], directory.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('fails if flutter build --config-only fails', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        processRunner.mockProcessesForExecutable[getFlutterCommand(
          mockPlatform,
        )] = <FakeProcessInfo>[
          FakeProcessInfo(MockProcess(), <String>['precache']),
          FakeProcessInfo(MockProcess(), <String>['repo', 'update']),
          FakeProcessInfo(MockProcess(exitCode: 1), <String>[
            'build',
            'ios',
            '--config-only',
          ]),
        ];

        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>['fetch-deps', '--no-dart', '--ios'],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('The following packages had errors:'),
          ]),
        );
      });

      test('skips non-iOS plugins', () async {
        createFakePlugin('plugin1', packagesDir);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--ios',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native iOS dependencies.'),
          ]),
        );
      });

      test('skips non-inline plugins', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformIOS: const PlatformDetails(PlatformSupport.federated),
          },
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--ios',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native iOS dependencies.'),
          ]),
        );
      });
    });

    group('macos', () {
      test('runs on all examples', () async {
        final examples = <String>['example1', 'example2'];
        final RepositoryPackage plugin = createFakePlugin(
          'plugin1',
          packagesDir,
          examples: examples,
          platformSupport: <String, PlatformDetails>{
            platformMacOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        final Iterable<Directory> exampleDirs = plugin.getExamples().map(
          (RepositoryPackage example) => example.directory,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--macos',
        ]);

        expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            const ProcessCall('flutter', <String>['precache', '--macos'], null),
            const ProcessCall('pod', <String>['repo', 'update'], null),
            for (final Directory directory in exampleDirs)
              ProcessCall('flutter', const <String>[
                'build',
                'macos',
                '--config-only',
              ], directory.path),
          ]),
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Running for plugin1'),
            contains('No issues found!'),
          ]),
        );
      });

      test('fails if flutter build --config-only fails', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformMacOS: const PlatformDetails(PlatformSupport.inline),
          },
        );

        processRunner.mockProcessesForExecutable[getFlutterCommand(
          mockPlatform,
        )] = <FakeProcessInfo>[
          FakeProcessInfo(MockProcess(), <String>['precache']),
          FakeProcessInfo(MockProcess(), <String>['repo', 'update']),
          FakeProcessInfo(MockProcess(exitCode: 1), <String>[
            'build',
            'macos',
            '--config-only',
          ]),
        ];

        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>['fetch-deps', '--no-dart', '--macos'],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('The following packages had errors:'),
          ]),
        );
      });

      test('skips non-macOS plugins', () async {
        createFakePlugin('plugin1', packagesDir);

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--macos',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native macOS dependencies.'),
          ]),
        );
      });

      test('skips non-inline plugins', () async {
        createFakePlugin(
          'plugin1',
          packagesDir,
          platformSupport: <String, PlatformDetails>{
            platformMacOS: const PlatformDetails(PlatformSupport.federated),
          },
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'fetch-deps',
          '--no-dart',
          '--macos',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('Package does not have native macOS dependencies.'),
          ]),
        );
      });
    });
  });
}
